After encapsulation and polymorphism, inheritance is the third major characteristic of all mature object-oriented programming languages. In Chapter 6, I briefly described what inheritance is and how it could be useful to programmers. And I also told you that—unfortunately—inheritance isn't natively supported by Visual Basic. In this section, I explain what you can do to remedy this deficiency.
Back to the Shapes sample program. This time, you'll write a CSquare class module, which adds support for drawing squares. Because this class is so similar to CRectangle, this could actually be a one-minute job: Just copy the CRectangle code into the CSquare module, and edit it where appropriate. For example, because a square is nothing but a rectangle with width equal to height, you could make both the Width and Height properties point to the same private variable.
This solution is somewhat unsatisfactory, however, because we have duplicated the code in the CRectangle class. If we later discover that the CRectangle class includes a bug, we must remember to correct it in the CSquare module, as well as in all other classes that were derived from CRectangle in the meantime. If Visual Basic supported true inheritance, we could just declare that the CSquare class inherits all its properties and methods from CRectangle, and then we could focus only on the few differences. Alas, this isn't possible, at least with the current version of Visual Basic. (I am an irrepressibly optimistic guy…) On the other hand, the concept of inheritance is so alluring and promising that you might take a second look at it. As I'll show shortly, you can resort to a coding technique that lets you simulate inheritance at the expense of some manual coding.
The technique of simulating inheritance is called delegation. The concept is simple: because most of the logic needed in CSquare (the derived class) is embodied in CRectangle (the base class), the code in CSquare can simply ask a CRectangle object to do the work on its behalf.
So you do this trick by declaring a private CRectangle object inside the CSquare class and passing it all the calls that CSquare doesn't want to deal with directly. These calls include all methods and all read/write operations for properties. Here's a possible implementation of this technique:
' The CSquare Class ' This is the Private instance of the CRectangle class. Private Rect As CRectangle Private Sub Class_Initialize() ' Create the private variable for doing the delegation. Set Rect = New CRectangle End Sub ' A simple pseudoconstructor for ease of use Friend Sub Init(Left As Single, Top As Single, Width As Single, _ Optional Color As Variant, Optional FillColor As Variant) ... End Sub ' The delegation code Property Get Left() As Single Left = Rect.Left End Property Property Let Left(ByVal newValue As Single) Rect.Left = newValue End Property Property Get Top() As Single Top = Rect.Top End Property Property Let Top(ByVal newValue As Single) Rect.Top = newValue End Property Property Get Width() As Single Width = Rect.Width End Property Property Let Width(ByVal newValue As Single) ' Squares are rectangles whose Width = Height. Rect.Width = newValue Rect.Height = newValue End Property Property Get Color() As Long Color = Rect.Color End Property Property Let Color(ByVal newValue As Long) Rect.Color = newValue End Property Property Get FillColor() As Long FillColor = Rect.FillColor End Property Property Let FillColor(ByVal newValue As Long) Rect.FillColor = newValue End Property |
Admittedly, it's a lot of code for such a simple task, but you shouldn't forget that we're playing with toy objects here. In a real program, the base class might include hundreds or thousands of lines of code. In that case, the relatively few lines needed for the delegation would be absolutely negligible.
While our CSquare class is functional, it still doesn't know how to redraw itself. If the CRectangle class exposed the Draw, Move, and Zoom methods in its primary interface—as it did in the first version of the Shapes program—this would have been child's play. Unfortunately, we moved the Draw method from the CRectangle main interface to its IShape secondary interface. For this reason, in order to delegate this method we first need to get a reference to that interface:
' In the CSquare class Private Sub IShape_Draw(pic As Object) Dim RectShape As IShape Set RectShape = Rect ' Retrieve the IShape interface. RectShape.Draw pic ' Now it works! End Sub |
Since you'll need a reference to Rect's IShape interface many times during the life of the CSquare class, you can speed up execution and reduce the amount of code by creating a module-level RectShape variable:
' CSquare also supports the IShape interface. Implements IShape ' This is the private instance of the CRectangle class. Private Rect As CRectangle ' This points the Rect's IShape interface. Private RectShape As IShape Private Sub Class_Initialize() ' Create the two variables for doing the delegation. Set Rect = New CRectangle Set RectShape = Rect End Sub ' ... code for Left, Top, Width, Color, FillColor properties ...(omitted) ' The IShape interface Private Sub IShape_Draw(pic As Object) RectShape.Draw pic End Sub Private Property Let IShape_Hidden(ByVal RHS As Boolean) RectShape.Hidden = RHS End Property Private Property Get IShape_Hidden() As Boolean IShape_Hidden = RectShape.Hidden End Property Private Sub IShape_Move(stepX As Single, stepY As Single) RectShape.Move stepX, stepY End Sub Private Sub IShape_Zoom(ZoomFactor As Single) RectShape.Zoom ZoomFactor End Sub |
While inheritance through delegation could be easily disregarded as a hack by any serious OO programmer working with mature OOPLs, the fact that you're in complete control of what happens during execution has several advantages. For example, when the client invokes a method in your derived class, you have several choices:
In the last two cases, your code is sometimes said to be subclassing the base class. It uses the base class for what can be useful but also executes some pre- and postprocessing code that adds power to the derived class. Even if the concept is vaguely similar, don't confuse it with control or Windows subclassing, which is a completely different (and more advanced) programming technique that lets you modify the behavior of standard Windows controls. (This type of subclassing is described in the Appendix.)
You might not be aware that VBA gives you the means to subclass itself. As you know, Visual Basic can be considered the sum of the Visual Basic library and the VBA language. These libraries are always present in the References dialog box and can't be removed as other external libraries can. Even if you can't remove them, however, as far as the Visual Basic parser is concerned, the names that you use in your own code have a higher priority than the names defined in external libraries, including the VBA library! To see what I mean, add this simple procedure in a standard BAS module:
' An IIf replacement that accepts just one argument ' If FalsePart is omitted and the expression is False, it returns Empty. Function IIf(Expression As Boolean, TruePart As Variant, _ Optional FalsePart As Variant) As Variant If Expression Then IIf = TruePart ElseIf Not IsMissing(FalsePart) Then IIf = FalsePart End If End Function |
You can call native VBA statements even if you're currently subclassing them, provided that you specify the name of the VBA library:
Function Hex(Value As Long, Optional Digits As Variant) As String If IsMissing(Digits) Then Hex = VBA.Hex(Value) Else Hex = Right$(String$(Digits, "0") & VBA.Hex(Value), Digits) End If End Function |
You should always try to keep the syntax of your new custom function compatible with that of the original VBA function so that you won't break any existing code.
One word of caution: This technique could give rise to problems, especially if you work on a team of programmers and not all of them are familiar with it. You can cope with this issue in part by always enforcing a compatible syntax, but this doesn't solve the problem when it falls to your colleagues to maintain or revise your code. For this reason, always consider the opportunity to define a new function with a different name and syntax so that your code isn't unnecessarily ambiguous.
If you completely inherit a class module from another class—that is, you implement all the methods of the base class into the derived class—you end up with two modules that are very similar to one another, often to the point that you can use an Object variable to leverage their polymorphism and simplify your client code. On the other hand, you know that you don't need to resort to late binding (that is, Object variables) to get all the advantages of polymorphism because secondary interfaces always offer a much better alternative.
As an illustration of this concept, the CSquare class could implement the CRectangle interface:
' In the CSquare class module Implements IShape Implements CRectangle ' The primary and the IShape interface are identical... (omitted).... ' This is the secondary CRectangle interface. Private Property Let CRectangle_Color(ByVal RHS As Long) Rect.Color = RHS End Property Private Property Get CRectangle_Color() As Long CRectangle_Color = Rect.Color End Property Private Property Let CRectangle_FillColor(ByVal RHS As Long) Rect.FillColor = RHS End Property Private Property Get CRectangle_FillColor() As Long CRectangle_FillColor = Rect.FillColor End Property ' The rect's Height property is replaced by the Width property. Private Property Let CRectangle_Height(ByVal RHS As Single) rect.Width = RHS End Property Private Property Get CRectangle_Height() As Single CRectangle_Height = rect.Width End Property Private Property Let CRectangle_Left(ByVal RHS As Single) Rect.Left = RHS End Property Private Property Get CRectangle_Left() As Single CRectangle_Left = Rect.Left End Property Private Property Let CRectangle_Top(ByVal RHS As Single) Rect.Top = RHS End Property Private Property Get CRectangle_Top() As Single CRectangle_Top = Rect.Top End Property Private Property Let CRectangle_Width(ByVal RHS As Single) Rect.Width = RHS End Property Private Property Get CRectangle_Width() As Single CRectangle_Width = Rect.Width End Property |
In the CRectangle interface, you're using the same delegation technique that you saw before, so actually this isn't much of a shift in the organization of the class module. The benefits of this approach, however, are visible in the client application, which can now refer to either a CRectangle or a CSquare object using a single variable and through early binding:
Dim figures As New Collection Dim rect As CRectangle, Top As Single ' Create a collection of rectangles and squares. figures.Add New_CRectangle(1000, 2000, 1500, 1200) figures.Add New_CSquare(1000, 2000, 1800) figures.Add New_CRectangle(1000, 2000, 1500, 1500) figures.Add New_CSquare(1000, 2000, 1100) ' Fill them, and stack them one over the other using early binding! For Each rect In figures rect.FillColor = vbRed rect.Left = 0: rect.Top = Top Top = Top + rect.Height Next |
When I introduced abstract classes as a means of defining interfaces, I said that abstract classes never contain executable code, but only the definition of the interface. But the previous example shows that it's perfectly legal to use the same class module as an interface blueprint for an Implements statement and at the same time use the code inside it.
The CRectangle class is a rather complex application of this technique because it works as a regular class, as a base class from which you can inherit, and as an interface that you can implement in other classes. When you begin to be acquainted with objects, this approach will become natural.
Inheritance is a great OOP technique that lets programmers derive new classes with minimum effort. Simulation of true inheritance through delegation is the next best thing, and even if it takes some coding effort you should always consider it when you're creating several classes that are similar to one another because inheritance lets you reuse code and logic, enforce a better encapsulation, and ease code maintenance: